/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.hiep;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;

/**
 *
 * @author hiepst
 */
@WebServlet(name = "GenerateDataServlet", urlPatterns = {"/GenerateDataServlet"})
public class GenerateDataServlet extends HttpServlet {

    private List<Crime> trainingData = new ArrayList<Crime>();
    private List<Crime> testingData = new ArrayList<Crime>();
    private List<Crime> predictingData = new ArrayList<Crime>();
    private List<Cluster> clusters = new ArrayList<Cluster>();
    // One hour in millisecond
    public static final int ONE_HOUR = 60 * 60 * 1000;
    private int k;
    private Logger logger = Logger.getLogger(GenerateDataServlet.class.getName());

//    static {
//        BasicConfigurator.configure();
//    }
//    @Override
//    public void init() throws ServletException {
//        super.init(); //To change body of generated methods, choose Tools | Templates.
////        BasicConfigurator.configure();
//
//        try {
//            PropertyConfigurator.configure(new FileInputStream("log4j.properties"));
//        } catch (FileNotFoundException ex) {
//            java.util.logging.Logger.getLogger(GenerateDataServlet.class.getName()).log(Level.SEVERE, null, ex);
//        }
//    }
    public void setTrainingData(List<Crime> trainingData) {
        this.trainingData = trainingData;
    }

    public void setTestingData(List<Crime> testingData) {
        this.testingData = testingData;
    }

    /**
     * Processes requests for both HTTP
     * <code>GET</code> and
     * <code>POST</code> methods.
     *
     * @param request servlet request
     * @param response servlet response
     * @throws ServletException if a servlet-specific error occurs
     * @throws IOException if an I/O error occurs
     */
    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        try {
            /* TODO output your page here. You may use following sample code. */
            out.println("<!DOCTYPE html>");
            out.println("<html>");
            out.println("<head>");
            out.println("<title>Servlet GenerateDataServlet</title>");
            out.println("</head>");
            out.println("<body>");
            out.println("<h1>Servlet GenerateDataServlet at " + request.getContextPath() + "</h1>");
            out.println("</body>");
            out.println("</html>");
        } finally {
            out.close();
        }
    }

    // <editor-fold defaultstate="collapsed" desc="HttpServlet methods. Click on the + sign on the left to edit the code.">
    /**
     * Handles the HTTP
     * <code>GET</code> method.
     *
     * @param request servlet request
     * @param response servlet response
     * @throws ServletException if a servlet-specific error occurs
     * @throws IOException if an I/O error occurs
     */
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

//        processRequest(request, response);

        BasicConfigurator.configure();
        String type = request.getParameter("type");

        // Generate training data.
        if (type.equals("training")) {
            generateTrainingData(request, response);
        }

        // Generate testing data.
        if (type.equals("testing")) {
            generateTestingData(request, response);
        }

        // Perform crime prediction
        if (type.equals("predicting")) {
            performPrediction(request, response);
        }

    }

    public static int generateRandomSuspects() {
        int randomNumber = (int) (Math.floor(Math.random() * 10) + 1);
        return randomNumber;
    }

    public static CrimeType generateCrimeType() {
        CrimeType[] types = CrimeType.values();
        return types[generateRandomIndex()];
    }

    public static int generateRandomIndex() {
        int randomIndex = (int) Math.floor(Math.random() * CrimeType.values().length);
        return randomIndex;
    }

    public static double generateLongitude() {
        double longitude = Math.random() * (-0.234) - 121.745;
        return longitude;
    }

    public static double generateLatitude() {
        double latitude = Math.random() * 0.204 + 37.228;
        return latitude;
    }

    /**
     * Handles the HTTP
     * <code>POST</code> method.
     *
     * @param request servlet request
     * @param response servlet response
     * @throws ServletException if a servlet-specific error occurs
     * @throws IOException if an I/O error occurs
     */
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    }

    /**
     * Returns a short description of the servlet.
     *
     * @return a String containing servlet description
     */
    @Override
    public String getServletInfo() {
        return "Short description";
    }// </editor-fold>

    private void generateTrainingData(HttpServletRequest request, HttpServletResponse response) throws IOException, NumberFormatException {
        // Reset to store new data.
        trainingData.clear();

        String trainingText = request.getParameter("training");
        int training = Integer.parseInt(trainingText);

        StringBuffer trainingStringBuffer = new StringBuffer();
        Date today = new Date();

        for (int i = 0; i < training; i++) {
            long preHour = today.getTime() - ONE_HOUR * i;

            Crime crime = new Crime();
            crime.setDate(new Date(preHour));
            crime.setLatitude(generateLatitude());
            crime.setLongitude(generateLongitude());
            crime.setSuspects(generateRandomSuspects());
            crime.setType(generateCrimeType());

            trainingData.add(crime);

            //Write response.
            trainingStringBuffer.append("<crime>");
            trainingStringBuffer.append("<latitude>" + crime.getLatitude() + "</latitude>");
            trainingStringBuffer.append("<longitude>" + crime.getLongitude() + "</longitude>");
            trainingStringBuffer.append("<type>" + crime.getType().getName() + "</type>");
            trainingStringBuffer.append("<date>" + crime.getDate().toString() + "</date>");
            trainingStringBuffer.append("<suspects>" + crime.getSuspects() + "</suspects>");
            trainingStringBuffer.append("<icon>" + crime.getType().getIcon() + "</icon>");
            trainingStringBuffer.append("</crime>");
        }
        response.setContentType("text/xml");
        response.setHeader("Cache-Control", "no-cache");
        response.getWriter().write("<training>" + trainingStringBuffer.toString() + "</training>");
        logger.info("generating data. Size = " + trainingData.size());
    }

    private void generateTestingData(HttpServletRequest request, HttpServletResponse response) throws IOException, NumberFormatException {
        // Reset to store new data.
        testingData.clear();

        String testingText = request.getParameter("testing");
        int testing = Integer.parseInt(testingText);

        StringBuffer testingStringBuffer = new StringBuffer();

        Date today = new Date();

        for (int i = 0; i < testing; i++) {
            long preHour = today.getTime() - ONE_HOUR * i;

            Crime crime = new Crime();
            crime.setDate(new Date(preHour));
            crime.setLatitude(generateLatitude());
            crime.setLongitude(generateLongitude());
            crime.setSuspects(generateRandomSuspects());
            crime.setType(generateCrimeType());

            testingData.add(crime);

            //Write response.
            testingStringBuffer.append("<crime>");
            testingStringBuffer.append("<latitude>" + crime.getLatitude() + "</latitude>");
            testingStringBuffer.append("<longitude>" + crime.getLongitude() + "</longitude>");
            testingStringBuffer.append("<type>" + crime.getType().getName() + "</type>");
            testingStringBuffer.append("<date>" + crime.getDate().toString() + "</date>");
            testingStringBuffer.append("<suspects>" + crime.getSuspects() + "</suspects>");
            testingStringBuffer.append("<icon>" + crime.getType().getIcon() + "</icon>");
            testingStringBuffer.append("</crime>");
        }
        response.setContentType("text/xml");
        response.setHeader("Cache-Control", "no-cache");
        response.getWriter().write("<testing>" + testingStringBuffer.toString() + "</testing>");
    }

    private void performPrediction(HttpServletRequest request, HttpServletResponse response) throws NumberFormatException, IOException {

        StringBuffer predictingStringBuffer = new StringBuffer();

        kMeans();

        perfomTesting();

        for (int i = 0; i < predictingData.size(); i++) {
            Crime crime = predictingData.get(i);

            //Write response.
            predictingStringBuffer.append("<crime>");
            predictingStringBuffer.append("<latitude>" + crime.getLatitude() + "</latitude>");
            predictingStringBuffer.append("<longitude>" + crime.getLongitude() + "</longitude>");
            predictingStringBuffer.append("<type>" + crime.getType().getName() + "</type>");
            predictingStringBuffer.append("<date>" + crime.getDate().toString() + "</date>");
            predictingStringBuffer.append("<suspects>" + crime.getSuspects() + "</suspects>");
            predictingStringBuffer.append("<icon>" + crime.getType().getIcon() + "</icon>");
            predictingStringBuffer.append("</crime>");
        }

        response.setContentType("text/xml");
        response.setHeader("Cache-Control", "no-cache");
        response.getWriter().write("<predicting>" + predictingStringBuffer.toString() + "</predicting>");
    }

    private double getDistanceFromLatLonInMile(double lat1, double lon1, double lat2, double lon2) {
        double km = getDistanceFromLatLonInKm(lat1, lon1, lat2, lon2);
        return km * 0.621371;
    }

    private double getDistanceFromLatLonInFoot(double lat1, double lon1, double lat2, double lon2) {
        double km = getDistanceFromLatLonInKm(lat1, lon1, lat2, lon2);
        return km * 3280.84;
    }

    /**
     *
     * @param lat1
     * @param lon1
     * @param lat2
     * @param lon2
     * @return
     */
    private double getDistanceFromLatLonInKm(double lat1, double lon1, double lat2, double lon2) {

        int R = 6371; // Radius of the earth in km
        double dLat = deg2rad(lat2 - lat1);  // deg2rad below
        double dLon = deg2rad(lon2 - lon1);
        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
                + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2))
                * Math.sin(dLon / 2) * Math.sin(dLon / 2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        double d = R * c; // Distance in km

        return d;
    }

    /**
     * Convert degree to radian.
     *
     * @param deg
     * @return
     */
    private double deg2rad(double deg) {
        return deg * (Math.PI / 180);
    }

    /**
     * Between 10% and 20% of training data.
     *
     * @return
     */
    public int generateRandomK() {
        int min = (int) (trainingData.size() * 0.1);
        int max = (int) (trainingData.size() * 0.2);
        int diff = max - min;
        int randomK = (int) Math.floor(Math.random() * diff + min);
        return randomK;
    }

    /**
     * K-means clustering algorithm.
     *
     */
    private void kMeans() {
        int iteration = 0;
        k = generateRandomK();
        logger.info("Performing k-means clustering. k = " + k);

        Date now = new Date();
        Date nextHour = new Date(now.getTime() + ONE_HOUR);

        // Reset clusters
        clusters.clear();

        // Create k clusters with their initial centroid
        for (int i = 0; i < k; i++) {
            Cluster cluster = new Cluster();

            // Randomly choose a location for centroid.
            Crime centroid = new Crime();
            centroid.setLatitude(generateLatitude());
            centroid.setLongitude(generateLongitude());
            centroid.setType(generateCrimeType());
            centroid.setDate(nextHour);
            centroid.setSuspects(generateRandomSuspects());

            cluster.setCentroid(centroid);
            clusters.add(cluster);
        }

        boolean isNotConverged = true;
        while (isNotConverged) {

            iteration++;

            // Clear cluster members.
            for (int i = 0; i < k; i++) {
                clusters.get(i).getMembers().clear();
            }

            assignToClosestCentroid();

            // Exit loop condition
            isNotConverged = false;

            // Recompute the centroid in each cluster.
            // Repeat if not converged.
            for (int i = 0; i < k; i++) {

                Crime centroid = clusters.get(i).getCentroid();

                double prevLat = centroid.getLatitude();
                double prevLong = centroid.getLongitude();

                double sumLat = 0;
                double sumLong = 0;
                List<Crime> members = clusters.get(i).getMembers();
                int total = members.size();

                for (int j = 0; j < total; j++) {
                    sumLat += members.get(j).getLatitude();
                    sumLong += members.get(j).getLongitude();
                }

                // Compute the means to get the new location for the centroid.
                double newLat = sumLat / total;
                double newLong = sumLong / total;

                if (iteration <= k
                        && getDistanceFromLatLonInFoot(newLat, newLong, prevLat, prevLong) > 500) {
                    centroid.setLatitude(newLat);
                    centroid.setLongitude(newLong);

                    if (isNotConverged == false) {
                        isNotConverged = true;
                    }
                }
            }


        }

        // Reset predicting data.
        predictingData.clear();

        // Add the converged centroids to prediction data.
        for (int i = 0; i < k; i++) {
            predictingData.add(clusters.get(i).getCentroid());
        }

        logger.info("Finished after iteration = " + iteration);
    }

    private void assignToClosestCentroid() {
        // Assign each data point to the closest centroid.
        for (int i = 0; i < trainingData.size(); i++) {
            Crime currentData = trainingData.get(i);

            double minDist = getDistanceFromLatLonInMile(
                    currentData.getLatitude(),
                    currentData.getLongitude(),
                    clusters.get(0).getCentroid().getLatitude(),
                    clusters.get(0).getCentroid().getLongitude());

            int minIndex = 0;

            for (int j = 0; j < k; j++) {

                double currentDist = getDistanceFromLatLonInMile(
                        currentData.getLatitude(),
                        currentData.getLongitude(),
                        clusters.get(j).getCentroid().getLatitude(),
                        clusters.get(j).getCentroid().getLongitude());

                if (currentDist < minDist) {
                    minDist = currentDist;
                    minIndex = j;
                }
            }

            // Assign current data point to the closes centroid.
            clusters.get(minIndex).add(currentData);
        }
    }

    public static void main(String args[]) {
        GenerateDataServlet servlet = new GenerateDataServlet();
        List<Crime> trainingData = new ArrayList<Crime>();
        List<Crime> testingData = new ArrayList<Crime>();

        int totalTest = 50;
        float sumAccuracy = 0;
        for (int i = 0; i < totalTest; i++) {

            trainingData.clear();
            testingData.clear();

            Date today = new Date();
            int training = 3500;
            int testing = 1500;
            for (int j = 0; j < training; j++) {
                long preHour = today.getTime() - ONE_HOUR * j;

                Crime crime = new Crime();
                crime.setDate(new Date(preHour));
                crime.setLatitude(generateLatitude());
                crime.setLongitude(generateLongitude());
                crime.setSuspects(generateRandomSuspects());
                crime.setType(generateCrimeType());

                trainingData.add(crime);
            }

            servlet.setTrainingData(trainingData);

            for (int k = 0; k < testing; k++) {
                long preHour = today.getTime() - ONE_HOUR * k;

                Crime crime = new Crime();
                crime.setDate(new Date(preHour));
                crime.setLatitude(generateLatitude());
                crime.setLongitude(generateLongitude());
                crime.setSuspects(generateRandomSuspects());
                crime.setType(generateCrimeType());

                testingData.add(crime);
            }

            servlet.setTestingData(testingData);

            servlet.kMeans();
            sumAccuracy += servlet.perfomTesting();
        }

        float meanAccu = (float) sumAccuracy / totalTest;
        System.out.println("Mean accuracy = " + meanAccu);

    }

    private float perfomTesting() {
        int total = testingData.size();
        int correct = 0;
        float accuracy = 0;

        for (int i = 0; i < total; i++) {
            for (int j = 0; j < predictingData.size(); j++) {
                double distance = getDistanceFromLatLonInFoot(
                        testingData.get(i).getLatitude(),
                        testingData.get(i).getLongitude(),
                        predictingData.get(j).getLatitude(),
                        predictingData.get(j).getLongitude());
                if (distance <= 1500) {
                    correct++;
                    break;
                }
            }
        }

        accuracy = (float) correct / total;
        logger.info("Accuracy=" + accuracy);

        return accuracy;
    }
}
